"use client" import * as React from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { ChevronDown, Gauge, MinusCircle, Octagon, ScrollText, ShieldAlert, Sigma, TriangleAlert, } from 'lucide-react' import { Page, PageBody } from '@open-mercato/ui/backend/Page' import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail' import { SectionHeader, CollapsibleSection } from '@open-mercato/ui/backend/SectionHeader' import { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay' import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert' import { Button } from '@open-mercato/ui/primitives/button' import { IconButton } from '@open-mercato/ui/primitives/icon-button' import { EmptyState } from '@open-mercato/ui/primitives/empty-state' import { Progress } from '@open-mercato/ui/primitives/progress' import { Spinner } from '@open-mercato/ui/primitives/spinner' import { StatusBadge } from '@open-mercato/ui/primitives/status-badge' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@open-mercato/ui/primitives/table' import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall' import { flash } from '@open-mercato/ui/backend/FlashMessages' import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent' import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation' import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context' import { cn } from '@open-mercato/shared/lib/utils' import { useCoalescedReload } from '../../../components/useCoalescedReload' import { formatDateTime, formatDurationMs } from '../../../components/types' import { agentLabelFor, useAgentLabelMap } from '../../../components/useAgentLabels' import { evalCaseRunStatusVariant, evalSuiteOutcomeVariant, evalSuiteStatusVariant, evalVerdictState, evalVerdictVariant, formatCaseRunCost, formatPassScore, formatScoreVariance, isActiveSuiteStatus, isTerminalCaseRunStatus, mapEvalAssertionResult, mapEvalCase, mapEvalCaseRun, mapEvalRunDetail, parseCaseRunStatus, readEvidenceMismatches, residualEvidence, type EvalAssertionResultRow, type EvalCaseRunRow, type EvalCaseView, type EvalRunDetailView, } from '../../../components/evalRunTypes' /** The route caps `pageSize` at 100; a suite can hold up to 500 case runs. */ const CASE_RUN_PAGE_SIZE = 100 type EvalRunDetailResponse = { run?: Record caseRuns?: Array> nextCursor?: string | null } type ResultsResponse = { items?: Array> } type ResultsState = { status: 'loading' | 'ready' | 'error' items: EvalAssertionResultRow[] } type EvalCaseState = { status: 'loading' | 'ready' | 'error' evalCase: EvalCaseView | null } function readEventString(payload: Record, key: string): string | null { const value = payload[key] return typeof value === 'string' && value.length > 0 ? value : null } /** Three-state verdict cell: `null` renders as SKIPPED — muted, never red. */ function VerdictBadge({ passed }: { passed: boolean | null }) { const t = useT() const state = evalVerdictState(passed) return ( {state === 'skipped' ? ( {t('agent_orchestrator.evalRuns.verdict.skipped')} ) : ( t(`agent_orchestrator.evalRuns.verdict.${state}`) )} ) } function HeaderStat({ label, value, icon: Icon, tone, }: { label: string value: string icon: React.ComponentType<{ className?: string }> tone?: 'default' | 'warning' }) { return (

{label}

{value}

) } /** * A mismatched value, rendered so `null`, `""` and a missing key stay * distinguishable — the whole point of reading a diff. */ function MismatchValue({ value }: { value: unknown }) { const t = useT() if (value === undefined) { return — } if (value === null) { return {t('agent_orchestrator.evalRuns.detail.valueNull')} } if (typeof value === 'string' && value.length === 0) { return {t('agent_orchestrator.evalRuns.detail.valueEmpty')} } const text = typeof value === 'string' ? value : JSON.stringify(value) return {text} } /** * Expected-vs-actual per diverging path. Without it the operator sees only WHICH * fields disagreed and has to open the trace and the eval case side by side to * learn HOW. */ function MismatchTable({ evidence }: { evidence: unknown }) { const t = useT() const { rows, omitted } = readEvidenceMismatches(evidence) if (rows.length === 0) return null const hasValues = rows.some((row) => row.expected !== undefined || row.actual !== undefined) return (
{t('agent_orchestrator.evalRuns.detail.mismatchPath')} {hasValues ? ( <> {t('agent_orchestrator.evalRuns.detail.mismatchExpected')} {t('agent_orchestrator.evalRuns.detail.mismatchActual')} ) : null} {rows.map((row) => ( {row.path} {hasValues ? ( <> ) : null} ))}
{omitted > 0 ? (

{t('agent_orchestrator.evalRuns.detail.mismatchOmitted', undefined, { count: omitted })}

) : null}
) } /** * The golden record the case run was scored against. Fetched per expanded row * from the single-case route, because `input`/`expected` are encrypted at rest * and are never projected into a list response. */ function GoldenCase({ state, evalCaseId }: { state: EvalCaseState; evalCaseId: string }) { const t = useT() if (state.status === 'loading') { return (
{t('agent_orchestrator.evalRuns.detail.goldenLoading')}
) } if (state.status === 'error' || !state.evalCase) { return

{t('agent_orchestrator.evalRuns.detail.goldenError')}

} const evalCase = state.evalCase return (
{evalCase.status} {evalCase.sourceType} {evalCase.processType ? ( {evalCase.processType} ) : null}
{evalCase.expected != null ? ( ) : (

{t('agent_orchestrator.evalRuns.detail.goldenNoExpected')}

)} {evalCase.input != null ? ( ) : null}
) } function AssertionResults({ state }: { state: ResultsState }) { const t = useT() const locale = useLocale() if (state.status === 'loading') { return (
{t('agent_orchestrator.evalRuns.detail.resultsLoading')}
) } if (state.status === 'error') { return } if (state.items.length === 0) { return

{t('agent_orchestrator.evalRuns.detail.resultsEmpty')}

} return (
    {state.items.map((result) => { const residual = residualEvidence(result.evidence) return (
  • {result.assertionKey} {t(`agent_orchestrator.evalRuns.severity.${result.severity}`)} {result.score != null ? ( {t('agent_orchestrator.evalRuns.detail.resultScore', undefined, { value: result.score.toFixed(2) })} ) : null} {formatDateTime(result.evaluatedAt, locale) ?? '—'}
    {residual != null ? (
    ) : null}
  • ) })}
) } export default function AgentEvalRunDetailPage({ params }: { params?: { id?: string } }) { const t = useT() const locale = useLocale() const router = useRouter() const agentLabels = useAgentLabelMap() const suiteRunId = params?.id ?? '' const [run, setRun] = React.useState(null) const [caseRuns, setCaseRuns] = React.useState([]) const [nextCursor, setNextCursor] = React.useState(null) const [isLoading, setIsLoading] = React.useState(true) const [isLoadingMore, setIsLoadingMore] = React.useState(false) const [error, setError] = React.useState(null) const [reloadToken, setReloadToken] = React.useState(0) const [expandedId, setExpandedId] = React.useState(null) const [results, setResults] = React.useState>({}) const [evalCases, setEvalCases] = React.useState>({}) const [isCancelling, setIsCancelling] = React.useState(false) const loadedRunRef = React.useRef(null) const { runMutation, retryLastMutation } = useGuardedMutation<{ retryLastMutation: () => Promise }>({ contextId: 'agent_orchestrator.evalRuns', blockedMessage: t('agent_orchestrator.proposal.flash.blocked'), }) React.useEffect(() => { let cancelled = false async function load() { // Live-driven reloads of an already-rendered suite refetch silently, so the // page does not flash a loading state while the operator is reading it. const silent = loadedRunRef.current === suiteRunId if (!silent) setIsLoading(true) setError(null) const call = await apiCall( `/api/agent_orchestrator/eval-runs/${encodeURIComponent(suiteRunId)}?pageSize=${CASE_RUN_PAGE_SIZE}`, undefined, { fallback: {} }, ) if (cancelled) return if (!call.ok || !call.result?.run) { setError(t('agent_orchestrator.evalRuns.detail.error')) setIsLoading(false) return } loadedRunRef.current = suiteRunId setRun(mapEvalRunDetail(call.result.run)) const items = Array.isArray(call.result.caseRuns) ? call.result.caseRuns : [] setCaseRuns(items.map((item) => mapEvalCaseRun(item)).filter((row): row is EvalCaseRunRow => row !== null)) setNextCursor(call.result.nextCursor ?? null) setIsLoading(false) } if (suiteRunId) void load() return () => { cancelled = true } }, [t, suiteRunId, reloadToken]) const loadMore = React.useCallback(async () => { if (!nextCursor) return setIsLoadingMore(true) const params = new URLSearchParams({ pageSize: String(CASE_RUN_PAGE_SIZE), after: nextCursor }) const call = await apiCall( `/api/agent_orchestrator/eval-runs/${encodeURIComponent(suiteRunId)}?${params.toString()}`, undefined, { fallback: {} }, ) if (!call.ok) { flash(t('agent_orchestrator.evalRuns.detail.loadMoreError'), 'error') setIsLoadingMore(false) return } const items = Array.isArray(call.result?.caseRuns) ? call.result.caseRuns : [] const mapped = items.map((item) => mapEvalCaseRun(item)).filter((row): row is EvalCaseRunRow => row !== null) setCaseRuns((current) => { const known = new Set(current.map((row) => row.id)) return [...current, ...mapped.filter((row) => !known.has(row.id))] }) setNextCursor(call.result?.nextCursor ?? null) setIsLoadingMore(false) }, [nextCursor, suiteRunId, t]) /** * Lazy by contract: the detail endpoint deliberately does not inline assertion * results, so they are fetched on FIRST expansion only. `requestedResultsRef` * makes that "first" exact — a re-expand never refires an in-flight request. */ const requestedResultsRef = React.useRef>(new Set()) /** * The golden case is fetched per eval case, NOT per case run: a suite at * repeatCount 3 expands three rows that share one case, and refetching the same * encrypted payload per trial buys nothing. */ const requestedCasesRef = React.useRef>(new Set()) const loadEvalCase = React.useCallback((evalCaseId: string) => { if (!evalCaseId || requestedCasesRef.current.has(evalCaseId)) return requestedCasesRef.current.add(evalCaseId) setEvalCases((current) => ({ ...current, [evalCaseId]: { status: 'loading', evalCase: null } })) void (async () => { const call = await apiCall>( `/api/agent_orchestrator/eval-cases/${encodeURIComponent(evalCaseId)}`, undefined, { fallback: {} }, ) const mapped = call.ok && call.result ? mapEvalCase(call.result) : null if (!mapped) { // Dropped from the requested set so collapsing and re-expanding retries. requestedCasesRef.current.delete(evalCaseId) setEvalCases((current) => ({ ...current, [evalCaseId]: { status: 'error', evalCase: null } })) return } setEvalCases((current) => ({ ...current, [evalCaseId]: { status: 'ready', evalCase: mapped } })) })() }, []) const toggleExpanded = React.useCallback((caseRunId: string, evalCaseId: string) => { setExpandedId((current) => (current === caseRunId ? null : caseRunId)) loadEvalCase(evalCaseId) if (requestedResultsRef.current.has(caseRunId)) return requestedResultsRef.current.add(caseRunId) setResults((current) => ({ ...current, [caseRunId]: { status: 'loading', items: [] } })) void (async () => { const call = await apiCall( `/api/agent_orchestrator/eval-runs/${encodeURIComponent(suiteRunId)}/case-runs/${encodeURIComponent(caseRunId)}/results`, undefined, { fallback: { items: [] } }, ) if (!call.ok) { // Dropped from the requested set so the operator can retry by collapsing // and re-expanding the row. requestedResultsRef.current.delete(caseRunId) setResults((current) => ({ ...current, [caseRunId]: { status: 'error', items: [] } })) return } const items = Array.isArray(call.result?.items) ? call.result.items : [] setResults((current) => ({ ...current, [caseRunId]: { status: 'ready', items: items .map((item) => mapEvalAssertionResult(item)) .filter((row): row is EvalAssertionResultRow => row !== null), }, })) })() }, [suiteRunId, loadEvalCase]) // Live progress: case-run events patch the loaded rows in place (no refetch of // a list the very run is still writing). The suite-completion event refreshes // the aggregates, coalesced against event bursts. const patchCaseRun = React.useCallback((payload: Record) => { if (readEventString(payload, 'suiteRunId') !== suiteRunId) return const caseRunId = readEventString(payload, 'id') if (!caseRunId) return setCaseRuns((current) => { const index = current.findIndex((row) => row.id === caseRunId) if (index < 0) return current const patched = [...current] const passed = payload.passed const score = payload.score patched[index] = { ...patched[index], status: parseCaseRunStatus(payload.status) ?? patched[index].status, passed: typeof passed === 'boolean' ? passed : null, score: typeof score === 'number' && Number.isFinite(score) ? score : null, } return patched }) }, [suiteRunId]) useAppEvent('agent_orchestrator.eval_case_run.started', (event) => { patchCaseRun(event.payload) }, [patchCaseRun]) useAppEvent('agent_orchestrator.eval_case_run.completed', (event) => { patchCaseRun(event.payload) }, [patchCaseRun]) const coalescedReload = useCoalescedReload( React.useCallback(() => setReloadToken((token) => token + 1), []), ) useAppEvent( 'agent_orchestrator.eval_suite_run.completed', (event) => { if (readEventString(event.payload, 'id') === suiteRunId) coalescedReload() }, [suiteRunId, coalescedReload], ) const cancelRun = React.useCallback(async () => { setIsCancelling(true) try { let nextStatus: string | null = null await runMutation({ operation: async () => { const call = await apiCallOrThrow<{ suiteRunId: string; status: string }>( `/api/agent_orchestrator/eval-runs/${encodeURIComponent(suiteRunId)}/cancel`, { method: 'POST' }, ) nextStatus = call.result?.status ?? null }, context: { retryLastMutation }, }) flash(t('agent_orchestrator.evalRuns.detail.cancelDone'), 'success') if (nextStatus) setReloadToken((token) => token + 1) } catch (err) { flash(err instanceof Error ? err.message : t('agent_orchestrator.evalRuns.detail.cancelError'), 'error') } finally { setIsCancelling(false) } }, [runMutation, retryLastMutation, suiteRunId, t]) const completedCount = React.useMemo( () => caseRuns.filter((row) => isTerminalCaseRunStatus(row.status)).length, [caseRuns], ) return (
{isLoading ? ( ) : error ? ( ) : !run ? ( ) : (
{t(`agent_orchestrator.evalRuns.status.${run.status}`)} {run.outcome ? ( {t(`agent_orchestrator.evalRuns.outcome.${run.outcome}`)} ) : null} {run.judgeMayGate ? t('agent_orchestrator.evalRuns.detail.judgeMayGate') : t('agent_orchestrator.evalRuns.detail.judgeAdvisory')} {t(`agent_orchestrator.evalRuns.trigger.${run.trigger}`)} {run.evalSetVersion ? ( {run.evalSetVersion} ) : null}

{agentLabelFor(agentLabels, run.agentDefinitionId)}

{run.agentDefinitionId}

{t('agent_orchestrator.evalRuns.detail.startedAt', undefined, { value: formatDateTime(run.startedAt ?? run.createdAt, locale) ?? '—', })} {run.finishedAt ? ` · ${t('agent_orchestrator.evalRuns.finishedAt', undefined, { value: formatDateTime(run.finishedAt, locale) ?? '—', })}` : ''}

{isActiveSuiteStatus(run.status) ? ( ) : null}
0 ? run.caseCount / run.repeatCount : run.caseCount, repeats: run.repeatCount, })} icon={ScrollText} /> 0 ? 'warning' : 'default'} />
{run.safetyRegressions.length > 0 ? ( }>
{t('agent_orchestrator.evalRuns.detail.safetyRegressionsTitle')} {t('agent_orchestrator.evalRuns.detail.safetyRegressionsDescription', undefined, { count: run.safetyRegressions.length, })}
    {run.safetyRegressions.map((key) => (
  • {key}
  • ))}
) : null} {isActiveSuiteStatus(run.status) ? (
0 ? Math.min(100, Math.round((completedCount / run.caseCount) * 100)) : 0} tone="accent" label={t('agent_orchestrator.evalRuns.detail.progressLabel', undefined, { completed: completedCount, total: run.caseCount, })} showValue /> {nextCursor ? (

{t('agent_orchestrator.evalRuns.detail.progressPartial')}

) : null}
) : null}
{caseRuns.length === 0 ? ( ) : (
{t('agent_orchestrator.evalRuns.col.status')} {t('agent_orchestrator.evalRuns.detail.trial')} {t('agent_orchestrator.evalRuns.detail.verdict')} {t('agent_orchestrator.evalRuns.detail.score')} {t('agent_orchestrator.evalRuns.detail.latency')} {t('agent_orchestrator.evalRuns.detail.cost')} {t('agent_orchestrator.evalRuns.detail.trace')} {caseRuns.map((caseRun) => { const expanded = expandedId === caseRun.id return ( toggleExpanded(caseRun.id, caseRun.evalCaseId)} > {t(`agent_orchestrator.evalRuns.caseStatus.${caseRun.status}`)} {caseRun.trialIndex + 1} {/* A case run that has not finished has NO verdict — which is not the same as `passed: null`, whose meaning is the positive claim "skipped: the assertion did not apply". Rendering a pending row as "Skipped" contradicts the status badge beside it and tells the operator a running suite skipped everything. */} {isTerminalCaseRunStatus(caseRun.status) ? ( ) : ( — )} {caseRun.score != null ? caseRun.score.toFixed(2) : '—'} {formatDurationMs(caseRun.latencyMs) ?? '—'} {formatCaseRunCost(caseRun.costMinor) ?? '—'} {caseRun.agentRunId ? ( ) : ( — )} {expanded ? ( {caseRun.errorMessage ? ( {caseRun.errorMessage} ) : null}
) : null}
) })}
)} {nextCursor ? ( ) : null}
{run.summary != null ? ( ) : null}
)}
) }